fix(tui): repair footer artifacts during agent startup and runtime#346
Open
abezzub-dr wants to merge 4 commits into
Open
fix(tui): repair footer artifacts during agent startup and runtime#346abezzub-dr wants to merge 4 commits into
abezzub-dr wants to merge 4 commits into
Conversation
Moat tells the child process the terminal is one row shorter than the host so the child doesn't paint its own bottom-pinned UI on the same row as moat's footer. Previously, manager.StartAttached and exec.go's ResizeTTY calls passed the full host terminal height. The status bar's DECSTBM scroll region was correctly set to lines 1..height-1, but the child still saw all height rows and drew its bottom UI at row height — colliding with the footer. Recent Claude Code versions paint a multi-line bottom UI (input prompt, model/workspace status, permissions hint), and every repaint clobbered moat's footer. Both processes then redrew on top of each other, producing character-interleaved artifacts in the content area. Add a reservedRows parameter to Manager.StartAttached. The manager subtracts it from the auto-detected InitialHeight. The CLI passes 1 when a status bar is present and subtracts 1 from the height in both ResizeTTY calls (initial post-start and SIGWINCH).
Some node-based TUIs (Claude Code among them) emit `\x1b[r` once during TTY normalization to reset the DECSTBM scroll region to full screen. That wipes out moat's scroll region — the bottom row is no longer reserved for the footer, and any subsequent footer redraw at row H becomes regular text that scrolls up with content. The visible symptom is moat's footer text appearing as scattered standalone lines inside Claude's content area. Detect `\x1b[r` in the byte stream, let it pass through to the terminal (the child may have meaningful side effects on it), then immediately reassert moat's scroll region wrapped in DECSC/DECRC so the cursor is not disturbed. Skip the reassert in compositor mode since the emulator owns its own screen state. Diagnosed from a TTY trace showing exactly one `\x1b[r` near startup and one at exit per session — this is a one-shot restore, not an ongoing fight with the child.
Pre-run hooks (e.g. `pnpm install`) and moat-init's own setup steps print to the same TTY as the user's command. TUIs that paint with relative cursor advances (\x1b[NC) instead of overwriting cells leave any prior characters bleeding through their layout — Claude Code's startup banner is a striking example, with pnpm install lines visible inside the logo glyphs. moat-init.sh now emits ESC[2J ESC[H after the pre_run hook (gated on a TTY and a successful hook — `set -e` aborts above on hook failure, so errors stay visible). The writer detects ESC[2J in the data stream and redraws the footer immediately, since the 50ms debounce isn't reliable during a busy agent startup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix three independent bugs that caused moat's footer to bleed into the agent's output. The visible symptoms were character-interleaved text in Claude's content area, the moat footer appearing as scattered standalone lines mid-conversation, and pnpm-install output bleeding through Claude's startup banner.
The bugs were diagnosed from a TTY trace (
moat claude --tty-trace) rather than guessed at — happy to share the trace if useful for review.Three fixes, each addressing a distinct cause
1. Reserve the status bar row in the PTY size advertised to the child (
ab5fa6f)Manager.StartAttachedandexec.go'sResizeTTYcalls passed the full host terminal height to the child. moat's DECSTBM scroll region was correctly set to lines1..H-1, but the child still saw allHrows and drew its bottom-pinned UI at rowH— colliding with moat's footer. Recent Claude Code versions paint a multi-line bottom UI (input prompt, model/workspace status, permissions hint), which made every repaint clobber the footer; both processes then redrew on top of each other, producing the character-interleaved artifacts.StartAttachedgains areservedRows uintparameter. The manager subtracts it from the auto-detectedInitialHeight. The CLI passes1when a status bar is in use and subtracts1from the height in bothResizeTTYcalls (initial post-start and SIGWINCH).2. Reassert the scroll region after the child resets DECSTBM (
ad972c5)Some node-based TUIs (Claude Code among them) emit
\x1b[ronce during TTY normalization to reset the scroll region to full screen. After that, moat's region is gone — any subsequent footer redraw at rowHbecomes regular text that scrolls up with content. The visible symptom is moat's footer text appearing as scattered standalone lines inside the agent's content area.The writer detects
\x1b[rin the byte stream, lets it pass through (some terminals normalize state on it), then immediately reasserts moat's scroll region wrapped in DECSC/DECRC so the cursor isn't disturbed. Skipped in compositor mode since the emulator owns its own state.3. Clear the screen between pre_run hooks and the user's command (
c51e191)Pre-run hooks (e.g.
pnpm install) and moat-init's own setup steps print to the same TTY as the user's command. TUIs that paint with relative cursor advances (\x1b[NC) instead of overwriting cells leave any prior characters bleeding through their layout — Claude Code's startup banner is a striking example, with pnpm install lines visible inside the logo glyphs.moat-init.shnow emits\x1b[2J\x1b[Hafter the pre_run hook, gated on a TTY (and on hook success —set -eaborts above on hook failure, so errors stay visible). The writer detects\x1b[2Jin the data stream and redraws the footer immediately, since the 50ms debounce isn't reliable during a busy startup.Test plan
go test -race ./internal/tui/passes (28 existing + 8 new tests, including DECSTBM reassert, erase-screen redraw, split-across-writes prefix detection, and compositor-mode skips)go test -race ./internal/run/ ./cmd/moat/cli/passesgolangci-lint run --new-from-rev=mainreports 0 new issuesmoat claude pricing-agentagainst a TTY trace: no character-interleaving in content, no scattered footer lines, banner renders on a clean screengo test -tags=e2e ./internal/e2e/(5 e2e callers updated to pass0reservedRows)--rebuildflag — since the script is embedded into the image at build time)